package com.tcleaner.table;

import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.slf4j.Logger;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CleanService {

    private Logger logger;
    private static final String DEFAULT_GROUP = "0";

    public CleanService(Logger logger) {
        this.logger = logger;
    }

    public void clean(Map<String, Config> configGroup) throws Exception {
        String cleanTag = getCleanTag();
        logger.info("cleanTag " + cleanTag);
        for (Map.Entry<String, Config> configEntry : configGroup.entrySet()) {
            String group = configEntry.getKey();
            Config config = configEntry.getValue();
            String tables = config.getTables().trim();
            tables = tables.replaceAll("(\\w+)\\s*(,)\\s*", "$1$2");
            String[] splitTables = tables.split(",");

            switch (config.getMode()) {
                case RENAME_AND_CREATE:
                    logger.info("start clean group[" + group + "]");
                    execute(cleanTag, config, splitTables, (table, statement) -> {
                        renameTable(table, statement, cleanTag);
                        createTable(table, statement, cleanTag);
                    });
                    break;
                case TRUNCATE:
                    logger.info("start truncate group[" + group + "]");
                    execute(cleanTag, config, splitTables, this::truncateTable);
                    break;
                case RECOVER:
                    logger.info("start recover group[" + group + "]");
                    execute(cleanTag, config, splitTables, (table, statement) -> {
                        renameTable(table, statement, cleanTag);
                    });
                    String sql = config.getSql();
                    config.setSql(null);
                    String recoverTag = config.getCleanTag();
                    List<String> tablesWithCleanTag = Arrays.stream(splitTables).map(t -> t + "_" + recoverTag).collect(Collectors.toList());
                    execute(recoverTag, config, tablesWithCleanTag.toArray(new String[0]), (rtable, rstatement) -> {
                        recoverTable(rtable, rstatement, recoverTag);
                    });
                    config.setSql(sql);
                    break;
            }
        }
        logger.info("clean table finish.");
    }

    private void recoverTable(String table, Statement statement, String cleanTag) throws SQLException {
        String targetTableName = table.substring(0, (table.length() - 1) - cleanTag.length());
        logger.info("rename table " + table + " to " + targetTableName);
        statement.execute("rename table " + table + " to " + targetTableName);
    }

    private void execute(String cleanTag, Config config, String[] splitTables, Caller caller) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (Connection connection = DriverManager.getConnection(config.getDbUrl(), config.getDbUser(), config.getDbPassword());
             Statement statement = connection.createStatement()) {
            Set<String> tableNames = checkTables(connection, splitTables);
            for (String tableName : tableNames) {
                caller.call(tableName, statement);
            }

            String sql = config.getSql();
            if (sql != null) {
                sql = sql.replace("{cleanTag}", cleanTag);
                logger.info("execute sql: " + sql);
                statement.execute(sql);
            }
        }
    }

    private Set<String> checkTables(Connection connection, String[] tables) throws SQLException {
        StringBuilder tableSql = new StringBuilder("select TABLE_SCHEMA,TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA = '");
        tableSql.append(connection.getCatalog()).append("' and TABLE_NAME in (");
        for (String tableName : tables) {
            tableSql.append("'").append(tableName).append("',");
        }
        tableSql.deleteCharAt(tableSql.length() - 1);
        tableSql.append(")");
        Set<String> tableNames = new HashSet<>();
        try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(tableSql.toString())) {
            while (resultSet.next()) {
                String tableName = resultSet.getString("TABLE_NAME");
                tableNames.add(tableName);
            }
        }
        Set<String> notExistsTables = Stream.of(tables).filter(t -> !tableNames.contains(t)).collect(Collectors.toSet());
        if (notExistsTables.size() > 0) {
            logger.info("tables " + notExistsTables + " not exists.");
        }
        return tableNames;
    }

    private void renameTable(String table, Statement statement, String cleanTag) throws Exception {
        logger.info("rename table " + table + " to " + table + "_" + cleanTag);
        statement.execute("rename table " + table + " to " + table + "_" + cleanTag);
    }

    private void createTable(String table, Statement statement, String cleanTag) throws SQLException {
        logger.info("create new table " + table);
        statement.execute("create table " + table + " like " + table + "_" + cleanTag);
    }

    private void truncateTable(String table, Statement statement) throws Exception {
        String sql = "TRUNCATE " + table;
        logger.info("truncate table " + table);
        statement.execute(sql);
    }

    private String getCleanTag() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
        return LocalDateTime.now().format(formatter);
    }

    public Map<String, Config> parseConfig(HelpFormatter hf, Options options, String file) {
        Map<String, Config> configs = new HashMap<>();

        if (file != null && file.length() > 0) {
            try (FileInputStream fis = new FileInputStream(file); InputStream in = new BufferedInputStream(fis)) {
                Properties propertiesAll = new Properties();
                propertiesAll.load(in);

                Map<String, Properties> configGroup = new HashMap<>();
                for (String name : propertiesAll.stringPropertyNames()) {
                    if (name.endsWith("]")) {
                        int startIndex = name.indexOf("[");
                        if (startIndex <= 0) {
                            logger.error("config " + name + " is not correct");
                            System.exit(1);
                        }
                        String configName = name.substring(0, startIndex);
                        if (configName.length() == 0) {
                            logger.error("config " + name + " is not correct");
                            System.exit(1);
                        }
                        String group = name.substring(startIndex + 1, name.indexOf("]"));
                        Properties config = configGroup.get(group);
                        if (config == null) {
                            config = new Properties();
                            configGroup.put(group, config);
                        }
                        config.put(configName, propertiesAll.get(name));
                    } else {
                        Properties config = configGroup.get(DEFAULT_GROUP);
                        if (config == null) {
                            config = new Properties();
                            configGroup.put(DEFAULT_GROUP, config);
                        }
                        config.put(name, propertiesAll.get(name));
                    }
                }

                for (Map.Entry<String, Properties> configEntry : configGroup.entrySet()) {
                    Config config = new Config();
                    properties2Object(configEntry.getValue(), config);

                    String mode = (String) propertiesAll.get("mode");
                    if (mode != null && mode.length() > 0) {
                        config.setMode(Mode.values()[Integer.parseInt(mode)]);
                    } else {
                        config.setMode(Mode.RENAME_AND_CREATE);
                    }

                    checkConfig(config);

                    configs.put(configEntry.getKey(), config);
                }
            } catch (Exception e) {
                logger.error("", e);
                System.exit(1);
            }
        } else {
            hf.printHelp("Config file path can not null", options, true);
            System.exit(1);
        }

        return configs;
    }

    private void checkConfig(Config config) {
        String tables = config.getTables();
        checkString("tables", tables);

        String dbUrl = config.getDbUrl();
        checkString("dbUrl", dbUrl);

        String dbUser = config.getDbUser();
        checkString("dbUser", dbUser);

        String dbPassword = config.getDbPassword();
        checkString("dbPassword", dbPassword);

        Mode mode = config.getMode();
        if (Mode.RECOVER == mode && (config.getCleanTag() == null)) {
            logger.error("recover mode, cleanTag can not null");
            System.exit(1);
        }
    }

    public void checkString(String name, String value) {
        if (value == null) {
            logger.error(name + " can not null");
            System.exit(1);
        }
    }

    public void properties2Object(final Properties p, final Object object) {
        Method[] methods = object.getClass().getMethods();
        for (Method method : methods) {
            String mn = method.getName();
            if (mn.startsWith("set")) {
                try {
                    String tmp = mn.substring(4);
                    String first = mn.substring(3, 4);

                    String key = first.toLowerCase() + tmp;
                    String property = p.getProperty(key);
                    if (property != null) {
                        property = property.trim();
                        if (property.length() == 0) {
                            continue;
                        }
                        Class<?>[] pt = method.getParameterTypes();
                        if (pt != null && pt.length > 0) {
                            String cn = pt[0].getSimpleName();
                            Object arg = null;
                            if (cn.equals("int") || cn.equals("Integer")) {
                                arg = Integer.parseInt(property);
                            } else if (cn.equals("long") || cn.equals("Long")) {
                                arg = Long.parseLong(property);
                            } else if (cn.equals("double") || cn.equals("Double")) {
                                arg = Double.parseDouble(property);
                            } else if (cn.equals("boolean") || cn.equals("Boolean")) {
                                arg = Boolean.parseBoolean(property);
                            } else if (cn.equals("float") || cn.equals("Float")) {
                                arg = Float.parseFloat(property);
                            } else if (cn.equals("String")) {
                                arg = property;
                            } else {
                                continue;
                            }
                            method.invoke(object, arg);
                        }
                    }
                } catch (Throwable ignored) {
                }
            }
        }
    }

    public interface Caller {
        void call(String table, Statement statement) throws Exception;
    }
}
